// Copyright 2011 Google Inc. All Rights Reserved.

package naclrun

import (
	"archive/zip"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path"
	"strings"
	"syscall"
)

// ZipFS implements a read-only file system on top of a zip file.
type ZipFS struct {
	zip    *zip.ReadCloser
	names  map[string]*zip.File
	files  map[string]*os.File
	tmpDir string
}

// NewZipFS returns a new ZipFS using the given zip file name and tmpDir.
// Usually, tmpdir is an empty string, if /dev/shm presents on the file system.
func NewZipFS(filename, tmpDir string) (fs *ZipFS, err os.Error) {
	var rc *zip.ReadCloser
	if rc, err = zip.OpenReader(filename); err != nil {
		return
	}
	names := make(map[string]*zip.File)
	for _, item := range rc.File {
		names[item.Name] = item
	}
	fs = &ZipFS{
		zip:    rc,
		names:  names,
		files:  make(map[string]*os.File),
		tmpDir: tmpDir,
	}
	return
}

func (fs *ZipFS) putAndReopen(prefix string, data []byte) (f *os.File, err os.Error) {
	var w *os.File
	var filename string
	if w, err = ioutil.TempFile(fs.tmpDir, prefix); err != nil {
		return
	}
	filename = w.Name()
	if _, err = w.Write(data); err != nil {
		w.Close()
		return
	}
	if err = w.Close(); err != nil {
		return
	}
	f, err = os.Open(filename)
	// Unlink the file, because we want it to be closed after all readers are gone.
	os.Remove(filename)
	return
}

func (fs *ZipFS) get(filename string) (f *os.File, err os.Error) {
	var ok bool
	// Fast path
	if f, ok = fs.files[filename]; ok {
		return
	}
	var af *zip.File
	if af, ok = fs.names[filename]; !ok {
		return nil, fmt.Errorf("Could not find archive entry: %s", filename)
	}
	var rc io.ReadCloser
	if rc, err = af.Open(); err != nil {
		return
	}
	defer rc.Close()
	var data []byte
	if data, err = ioutil.ReadAll(rc); err != nil {
		return
	}
	f, err = fs.putAndReopen(fmt.Sprintf("nacl-glibc-%s-", path.Base(filename)), data)
	if err != nil {
		return
	}
	fs.files[filename] = f
	return
}

// Open implements FS.Open.
func (fs *ZipFS) Open(filename string, flags int, mode uint32) (f *os.File, err os.Error) {
	if !strings.HasPrefix(filename, "/nacl64/lib/") {
		return nil, fmt.Errorf("File not found: %s", filename)
	}
	if flags != os.O_RDONLY {
		return nil, fmt.Errorf("Only O_RDONLY is allowed on glibc files")
	}
	var ff *os.File
	if ff, err = fs.get(path.Join("glibc", filename)); err != nil {
		return
	}
	fd, errno := syscall.Dup(ff.Fd())
	if errno != 0 {
		return nil, os.NewSyscallError("dup", errno)
	}
	return os.NewFile(fd, ff.Name()), nil
}

// Close closes all the cached files and the underlying zip file.
func (fs *ZipFS) Close() os.Error {
	for _, f := range fs.files {
		f.Close()
	}
	return fs.zip.Close()
}
